package net.sf.ivyide.idea;

import com.intellij.openapi.application.Application;
import com.intellij.openapi.application.ApplicationManager;
import com.intellij.openapi.diagnostic.Logger;
import com.intellij.openapi.progress.ProcessCanceledException;
import com.intellij.openapi.progress.ProgressIndicator;
import com.intellij.openapi.progress.ProgressManager;
import com.intellij.openapi.progress.util.BlockingProgressIndicator;
import com.intellij.openapi.progress.util.ProgressWindow;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.util.Computable;
import com.intellij.openapi.wm.WindowManager;

import javax.swing.*;
import java.awt.*;
import java.lang.reflect.InvocationTargetException;

public class ProgressUtilities {

    public static void executeAsWriteActionOnEventQueue(final ProgressAwareRunnable runnable, ProgressIndicator rootIndicator) {
        executeAsWriteActionOnEventQueue(new ProgressAwareCallable<Void>() {
            public Void call(ProgressIndicator progressIndicator) throws Exception {
                runnable.run(progressIndicator);
                return null;
            }
        }, rootIndicator);
    }

    public static <T> T executeAsWriteActionOnEventQueue(final ProgressAwareCallable<T> runnable, final ProgressIndicator indicator) {
        try {
            if (!EventQueue.isDispatchThread()) {
                ProgressAwareCallable<T> r = wrapAsWriteAction(runnable);
                final CallableToRunnableBridge<T> decorator = new CallableToRunnableBridge<T>(r);
                EventQueue.invokeAndWait(new Runnable() {
                    public void run() {
                        decorator.run(indicator);
                    }
                });
                return decorator.getResult();
            } else {
                if (ApplicationManager.getApplication().isWriteAccessAllowed()) {
                    return runnable.call(indicator);
                } else {
                    return ApplicationManager.getApplication().runWriteAction(new MyComputable<T>(runnable, indicator));
                }
            }
        } catch (RuntimeException e) {
            throw e;
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }

    private static <T> ProgressAwareCallable<T> wrapAsWriteAction(final ProgressAwareCallable<T> runnable) {
        return new ProgressAwareCallable<T>() {
            public T call(final ProgressIndicator indicator) throws Exception {
                return ApplicationManager.getApplication().runWriteAction(new MyComputable<T>(runnable, indicator));
            }
        };
    }

    public static <T> T executeAsReadAction(final ProgressAwareCallable<T> runnable, final ProgressIndicator indicator) {
        try {
            if (ApplicationManager.getApplication().isReadAccessAllowed()) {
                return runnable.call(indicator);
            } else {
                return ApplicationManager.getApplication().runReadAction(new MyComputable<T>(runnable, indicator));
            }
        } catch (RuntimeException e) {
            throw e;
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }

    public static <T> T executeRunnableWithProgress(JComponent component, String progressMessage, final String errorMessage, final ProgressAwareCallable<T> runnable) {
        return runProcessWithProgressSynchronously(runnable, progressMessage, errorMessage, false, null, component);
    }

    public static void executeRunnableWithProgress(final JComponent component, final ProgressAwareRunnable decorator, final String progressMessage, final boolean cancelable) {
        if (!EventQueue.isDispatchThread()) {
            try {
                EventQueue.invokeAndWait(new Runnable() {
                    public void run() {
                        runProcessWithProgressSynchronously(component, decorator, progressMessage, cancelable, null);
                    }
                });
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            } catch (InvocationTargetException e) {
                throw new RuntimeException(e);
            }
        } else {
            runProcessWithProgressSynchronously(component, decorator, progressMessage, cancelable, null);
        }
    }

    //if run in swing thread, progress messages showing scanning of new files etc are not shown.
    //if not run in swing thread, commit fails. (should be executed in swing thread)

    //If applicaitonMnager.runWithProgress.. is executed nested, it uses the *current* thread, which can be the swing thread !
    //eg: runWithProgress starts new thread I and stores this thread, nested go back to the swing thread and call runWithProgress
    //again. It will reuse see that a progress is already running,since I is set, and will just run the runnable in the current thread,
    //assuming the current thread equals I, however, it isnt (its the swing thread). Calling ProgressManager.getIndicator will return null, since no progressindiciator
    //is attached to the current (swing) thread.

    //Very strange. If executed non nested, it creates a new thread and blocks the swing thread with the progress window.
    //only one prgoresswindow is created.

    //progress window itself launches another thread to

    //Calling *setText* on progress window should only be done from non swing thread.

    //So: start swing thread. Open progress windows, for long term stuff, readonly stuff, do executeBackgroud.
    //In background, call update progressindicator.

    private static class RunnableToCallableBridge<T> implements ProgressAwareCallable<T> {

        private ProgressAwareRunnable m_runnable;
        private boolean m_success = false;

        public RunnableToCallableBridge(ProgressAwareRunnable runnable) {
            m_runnable = runnable;
        }

        public T call(ProgressIndicator indicator) throws Exception {
            m_runnable.run(indicator);
            m_success = true;
            return null;
        }
    }

    public static boolean runProcessWithProgressSynchronously(JComponent component, final ProgressAwareRunnable process, final String s, boolean cancelable, final Project project) {
        RunnableToCallableBridge<Void> callable = new RunnableToCallableBridge<Void>(process);
        runProcessWithProgressSynchronously(callable, s, null, cancelable, project, component);
        return !callable.m_success;
    }

    private static <T> T runProcessWithProgressSynchronously(final ProgressAwareCallable<T> process, final String s, String errorMessage, boolean cancelable, final Project project, final JComponent component) {
        final CallableToRunnableBridge<T> decorator = new CallableToRunnableBridge<T>(process);
        runProcessWithProgressSynchronouslyNoErrorHandling(decorator, s, cancelable, project, component);
        try {
            return decorator.getResult();
        } catch (Exception exception) {
            showException(component != null ? component : WindowManager.getInstance().suggestParentWindow(project), errorMessage, exception);
            return null;
        }
    }


    public static void runProcessWithProgressSynchronouslyNoErrorHandling(final ProgressAwareRunnable process, final String title, boolean cancelable, final Project project, final JComponent parent) {
        //this works, but the idea progress bars are different (not reused)
        //And the progress bar only start animating after a while (5 seconds)
        //Also the progress bar shows a annoying hint menu
        final ProgressIndicator progress;
        ProgressWindow progresswindow = new ProgressWindow(cancelable, false, project, parent, null);
        progresswindow.setTitle(title);
        //effect of smooth adapter. Smoother progress bar animation + first delay ??
        //progress = new SmoothProgressAdapter(progresswindow, project);
        progress = progresswindow;

        final Runnable target = new Runnable() {

            public void run() {
                try {
                    ProgressManager.getInstance().runProcess(new Runnable() {
                        public void run() {
                            process.run(progress);
                        }
                    }, progress);
                }
                catch (ProcessCanceledException processcanceledexception) {
                }
            }
        };
        //progress should only start after progress monitoring has started blocking.
        //otherwise, race conditions may occur leading to intelilj assert failures...
        //so use invoke later
        //This only executes the target after model windows have been removed. Leads to deadlock, since startblocking
        //method keeps the modal window open.
        //ApplicationManager.getApplication().invokeLater(target);
        SwingUtilities.invokeLater(new Runnable() {
            public void run() {
                Application application = ApplicationManager.getApplication();
                application.executeOnPooledThread(target);
            }
        });
        ((BlockingProgressIndicator) (progress)).startBlocking();
    }


    public static void showException(Component component, String errorMessage, Exception exception) {
        Logger.getInstance(ProgressUtilities.class.getName()).warn(errorMessage, exception);
        JOptionPane.showMessageDialog(component, exception.getLocalizedMessage(), errorMessage, JOptionPane.ERROR_MESSAGE);
    }

    public static class CallableToRunnableBridge<T> implements ProgressAwareRunnable {

        private ProgressAwareCallable<T> m_runnable;
        private Exception m_exception;
        private T m_result;

        public CallableToRunnableBridge(ProgressAwareCallable<T> runnable) {
            m_runnable = runnable;
        }

        public T getResult() throws Exception {
            if (m_exception != null) {
                throw m_exception;
            }
            return m_result;
        }

        public void run(ProgressIndicator indicator) {
            try {
                m_result = m_runnable.call(indicator);
            } catch (Exception e) {
                m_exception = e;
            }
        }
    }

    private static class MyComputable<T> implements Computable<T> {
        private final ProgressAwareCallable<T> runnable;
        private final ProgressIndicator indicator;

        public MyComputable(ProgressAwareCallable<T> runnable, ProgressIndicator indicator) {
            this.runnable = runnable;
            this.indicator = indicator;
        }

        public T compute() {
            try {
                return runnable.call(indicator);
            } catch (RuntimeException e) {
                throw e;
            } catch (Exception e) {
                throw new RuntimeException(e);
            }
        }
    }
}
